package name.williamwall.buxoid;
/**
 * This software is made available under the Gnu General Public License v3
 * and has absolutely no waranty. You may change the software to suit your needs.
 * 
 * Author: William Wall, IV
 */
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import name.williamwall.buxoid.trust.FakeSocketFactory;

import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.params.ClientPNames;
import org.apache.http.client.params.CookiePolicy;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.HTTP;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import android.app.Activity;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.view.Display;

 
/**
 * This class interfaces with the Buxfer.com api and downloads
 * account lists, transactions, tags, and more.
 * 
 * @author William A. Wall, IV
 *
 */

public class Buxfer {
	private String mToken = null;
	private DefaultHttpClient client;
	private DocumentBuilder builder;
	private NotificationManager mNM = null;
	private BuxoidDb mDb;
	private static final NumberFormat money = NumberFormat.getCurrencyInstance();
	private static final DecimalFormat df = new DecimalFormat("0.00");
	private static final String URI = "https://www.buxfer.com/api/";
	
	private static final String[] MONTHS = new String [] {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
	
	public Buxfer (BuxoidDb db) {
		mDb = db;
		
        SchemeRegistry registry = new SchemeRegistry();
        registry.register(new Scheme("http", new PlainSocketFactory(), 80));
        
        // we'll register our own socket factory with a fake trust manager to get 
        // around the fact that the Android team removed buxfer's certificate 
        // authority from the Android Trust Store
        registry.register(new Scheme("https", new FakeSocketFactory(), 443));
        
        HttpParams httpParams = new BasicHttpParams();
        
        client = new DefaultHttpClient(new ThreadSafeClientConnManager(httpParams, registry), httpParams);
        client.getParams().setParameter(ClientPNames.COOKIE_POLICY, CookiePolicy.BROWSER_COMPATIBILITY);
		try {
			 builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	public void setNotificationManager(NotificationManager nm) {
		this.mNM = nm;
	}
	
	/**
	 * Logs in to the Buxfer.com api and preserves the login token for use 
	 * in retrieving different sets of data.
	 * 
	 * @param user 
	 * @param pass
	 * @throws BuxException
	 * @throws IOException
	 * @throws SAXException
	 * @throws URISyntaxException 
	 */
	public void login (String user, String pass) throws BuxException, IOException, SAXException, URISyntaxException {
//		Log.d(getClass().getSimpleName(), "Logging in...");
		
		// execute secure request and build xml response document
		HttpGet get = new HttpGet(new URI(URI + "login.xml?userid="+URLEncoder.encode(user)+"&password="+URLEncoder.encode(pass)));
		HttpResponse response = client.execute(get);
		Document doc = builder.parse(response.getEntity().getContent());
		
		// get status from xml
		String status = getTextContentByTagName(doc, "status");
		
		if (status != null && status.equals("OK")) {
			mToken = getTextContentByTagName(doc, "token");
		} else {
			// wrong user name or password
			throw new BuxException(status);
		}
//		Log.d(getClass().getSimpleName(), "token=" + mToken);
	}
	
	/**
	 * Downloads all accounts for the current user.
	 * 
	 * @throws BuxException
	 * @throws IOException
	 * @throws SAXException
	 * @throws URISyntaxException 
	 */
	public void downloadAccounts() throws BuxException, IOException, SAXException, URISyntaxException {
//		Log.d(getClass().getSimpleName(), "Downloading accounts...");
		int count = 0;
		
		if (mToken != null) {
			// delete all accounts
			mDb.deleteAllAccounts();
			
			// execute secure http request for the account data as xml
			HttpGet get = new HttpGet(new URI(URI + "accounts.xml?token="+mToken));
			HttpResponse response = client.execute(get);
			
			// build xml document
			Document doc = builder.parse(response.getEntity().getContent());
			
			// get the status of the response
			String status = getTextContentByTagName(doc, "status");
			
			if (status.equals("OK")) {
				// get a list of all the <account> elements
				NodeList accounts = doc.getElementsByTagName("account");
				
				String id, name, bank, lastSynced;
				double balance;
				
				NodeList properties;
				Node property;	
				
				// go through list of <account> elements, parsing the properties of that account and adding it to the db
				for (int i = 0; i < accounts.getLength(); i++) {
					properties = accounts.item(i).getChildNodes();
					
					// reset variables
					id = null;
					name = null;
					bank = null;
					lastSynced = null;
					balance = 0;
					
					for (int j = 0; j < properties.getLength(); j++) {
						property = properties.item(j);
						
						if (property.getNodeName().equals("id")) {
							id = getTextContent(property);
						} else if (property.getNodeName().equals("name")) {
							name = getTextContent(property);
						} else if (property.getNodeName().equals("bank")) {
							bank = getTextContent(property);
						} else if (property.getNodeName().equals("balance")) {
							balance = Double.parseDouble(getTextContent(property));
						} else if (property.getNodeName().equals("lastSynced")) {
							lastSynced = getTextContent(property);
						}
					}
					
					// add the buxfer id to the list to return
					count++;
					
//					Log.d(getClass().getSimpleName(), "\t" + name + " - " + id);
					
					// add the account to the db
					mDb.createAccount(count, id, name, bank, balance, lastSynced);
					
					if (mDb.getSetting(BuxoidDb.SETTING_NOTIFY_BALANCE).equals("1") && mNM != null) {
						// In this sample, we'll use the same text for the ticker and the expanded notification
				        CharSequence text = name + ": " + money.format(balance);

				        // Set the icon, scrolling text and timestamp
				        Notification notification = new Notification(android.R.drawable.stat_notify_sync, text, System.currentTimeMillis());

				        // The PendingIntent to launch our activity if the user selects this notification
				        Intent intent = new Intent(mDb.getContext(), Buxoid.class);
				        
				        // accounts is the default tab, so just stick in the intent
				        PendingIntent contentIntent = PendingIntent.getActivity(mDb.getContext(), 0, intent, 0);

				        // Set the info for the views that show in the notification panel.
				        notification.setLatestEventInfo(mDb.getContext(), "Buxoid", text, contentIntent);

				        // Send the notification.
				        // We use a layout id because it is a unique number.  We use it later to cancel.
				        mNM.notify(count, notification);
					}
				}
//				Log.d(getClass().getSimpleName(), "Done.");
			} else {
				throw new BuxException(status);
			}
		} else {
			// not logged in
			throw new BuxException("ERROR: Not logged in.");
		}
	}
	
	public void downloadAnalysis() throws BuxException, IOException, SAXException, URISyntaxException {
		Calendar cal = Calendar.getInstance();
		int month = cal.get(Calendar.MONTH);
		int year = cal.get(Calendar.YEAR);
		
		// see if we need to do full first run
		if (mDb.getAnalysisCount() == 0) {
			// get month and year from oldest transaction
			String date = mDb.getOldestTransDate();
			if (date == null) {
			    return;
			}
			String[] parts = date.split("-");
			int oldYear = Integer.parseInt(parts[0]);
			int oldMonth = Integer.parseInt(parts[1]) - 1;
			
			// get analysis for each month starting with the oldest
			do {
//				Log.d("Buxfer", "oldMonth " + oldMonth + " oldYear " + oldYear);
				
				downloadAnalysis(oldMonth, oldYear);
				
				// increment month/year
				oldMonth++;
				if (oldMonth == 12) {
					oldYear++;
					oldMonth = 0;
				}
			} while (oldMonth <= month || oldYear < year);
		} else {
			// if not first run, then just update analysis for the current and previous month
			downloadAnalysis(month, year);
			month--;
			if (month == -1) {
			    month = 11;
			    year--;
			}
			downloadAnalysis(month, year);
		}
	}
	
	private void downloadAnalysis(int month, int year) throws BuxException, IOException, SAXException, URISyntaxException {
//		Log.d(getClass().getSimpleName(), "Downloading analysis...");
		if (mToken != null) {
			String options = "&type=expense";
			options += "&month=" + MONTHS[month] + "" + year;
			
//			Log.d(getClass().getSimpleName(), "options " + options);
			
			HttpGet get = new HttpGet(new URI(URI + "reports.xml?token=" + mToken + options));
			HttpResponse response = client.execute(get);
			
			Document doc = builder.parse(response.getEntity().getContent());
			
			String status = getTextContentByTagName(doc, "status");
			
			if (status.equals("OK")) {
				// change month from 0-11 to 1-12
				month++;
				mDb.deleteAnalysisMonth(month, year);
				NodeList items = doc.getElementsByTagName("item");
				NodeList properties;
				Node property;
				
				String name, tag, tagId;
				double amount, max = 0;
				
//				Log.d(getClass().getSimpleName(), items.getLength() + " analysis entries returned");
				// create an analysis entry for each tag returned
				for (int i = 0; i < items.getLength(); i++) {
					properties = items.item(i).getChildNodes();
					
					name = null;
					tag = null;
					tagId = null;
					amount = 0;
					
					for (int j = 0; j < properties.getLength(); j++) {
						property = properties.item(j);
						name = property.getNodeName();
						
						if (name.equals("tag")) {
							tag = getTextContent(property);
						} else if (name.equals("amount")) {
							amount = Double.parseDouble(getTextContent(property));
						} else if (name.equals("tagId")) {
							tagId = getTextContent(property);
						}
					}
					long id = mDb.createAnalysis(month, year, tagId, tag, "", amount);
					max = amount > max ? amount : max;
					if (mDb.getSetting(BuxoidDb.SETTING_NOTIFY_BUDGET).equals("1") && mNM != null) {
						double limit = mDb.getBudgetLimit(tagId);
						if (limit > 0 && amount > limit) {
					        CharSequence text = tag + " budget exceeded.";
	
					        // Set the icon, scrolling text and timestamp
					        Notification notification = new Notification(android.R.drawable.stat_notify_sync, text, System.currentTimeMillis());
	
					        // The PendingIntent to launch our activity if the user selects this notification
					        Intent intent = new Intent(mDb.getContext(), Buxoid.class);
					        intent.putExtra("tab", "budgets");
					        
					        PendingIntent contentIntent = PendingIntent.getActivity(mDb.getContext(), 0, intent, 0);
	
					        // Set the info for the views that show in the notification panel.
					        notification.setLatestEventInfo(mDb.getContext(), "Buxoid", text, contentIntent);
	
					        // Send the notification.
					        // We use a layout id because it is a unique number.  We use it later to cancel.
					        mNM.notify(year*10000+month*100+(int) id, notification);
						}
					}
				}
//				Log.d(getClass().getSimpleName(), "created analysis for " + month + " " + year);
				// now create the chart and legend for the analysis view
				
				// create the "other" entry for the legend if necessary
				mDb.createAnalysisOther(month, year);
				
				// pull data and create parameters for google chart api
				Cursor c = mDb.fetchAnalysisLegend(month, year);
				String chd = "&chd=", chco = "&chco=";
				int i;
				if (c.getCount() > 0 && c.moveToFirst()) {
					do {
						i = c.getPosition();
						chd += i == 0 ? "t:": ",";
						chd += df.format(c.getDouble(c.getColumnIndex(BuxoidDb.ANALYSIS_AMOUNT)) / max );
						chco += (i == 0 ? "": ",") + c.getString(c.getColumnIndex(BuxoidDb.ANALYSIS_COLOR));
					} while (c.moveToNext());
				}
				
				Display d;
				int width;
                int height;
                
				try {
				    d = ((Activity) mDb.getContext()).getWindowManager().getDefaultDisplay();
				    width = d.getWidth();
				    height = d.getHeight();
				} catch (ClassCastException e) {
				    // for now assume G1 width and height
				    width = 320;
				    height = 480;
				}
				
				width = Math.min(width, height) / 2;
				
				// create Google chart api uri
				String imageUri = "http://chart.apis.google.com/chart?cht=p3&chf=bg,s,00000000&chs=" + width + "x" + width + chd + chco;
//				Log.d("Buxfer", imageUri);
				
				// get the application context from the db
				Context context = mDb.getContext();
				
				// get the image from google chart api
				get = new HttpGet(imageUri.trim());
				response = client.execute(get);
				InputStream in = response.getEntity().getContent();
				
				// open file output stream
				FileOutputStream fileOut = context.openFileOutput("analysis"+year+month+".png", Context.MODE_PRIVATE);
				
				// write each byte from input stream to file
				int b;
				while ((b = in.read()) > -1) {
					fileOut.write(b);
				}
				
				// flush file buffer and close file output stream
				fileOut.flush();
				fileOut.close();
			} else {
				throw new BuxException(status);
			}
		} else {
			throw new BuxException("ERROR: Not logged in.");
		}
	}
	
	public void downloadImpacts() throws BuxException, IOException, SAXException {
//		Log.d(getClass().getSimpleName(), "Downloading impacts...");
		if (mToken != null) {
			int count = 0;
			
			// execute the secure http request
			HttpGet get = new HttpGet(URI + "loans.xml?token="+mToken);
			HttpResponse response = client.execute(get);
			
			// parse response as xml document
			Document doc = builder.parse(response.getEntity().getContent());
			
			String status = getTextContentByTagName(doc, "status");
			
			if (status.equals("OK")) {
				// remove old impacts
				mDb.deleteAllImpacts();
				
				String entity, type, name;
				double amount;
				
				NodeList impacts = doc.getElementsByTagName("loan");
				NodeList properties;
				Node property;
				
				for (int i = 0; i < impacts.getLength(); i++) {
					properties = impacts.item(i).getChildNodes();
					
					entity = "";
					type = "";
					name = "";
					amount = 0;
					
					for (int j = 0; j < properties.getLength(); j++) {
						property = properties.item(j);
						name = property.getNodeName();
						
						if (name.equals("entity")) {
							entity = getTextContent(property);
						} else if (name.equals("type")) {
							type = getTextContent(property);
						} else if (name.equals("balance")) {
							amount = Double.parseDouble(getTextContent(property));
						}
					}
					
					// add impact to DB
					count++;
					mDb.createImpact(count, entity, type, Math.abs(amount), amount < 0 ? "owe": "get");
				}
			} else {
				throw new BuxException(status);
			}
		} else {
			throw new BuxException("ERROR: Not logged in.");
		}
		
//		Log.d(getClass().getSimpleName(), "done.");
	}
	
	/**
	 * Downloads all the transactions available the first time. If data exists, then it updates
	 * data for the past 30 days.
	 * @param month
	 * @param year
	 * @throws BuxException
	 * @throws IOException
	 * @throws SAXException
	 * @throws ParseException 
	 */
	public void downloadTransactions() throws BuxException, IOException, SAXException, ParseException {
//		Log.d(getClass().getSimpleName(), "Downloading transactions");
		int count = 0; // how many transactions we've processed so far
		int num = 1;  // total number of transactions
		int page = 0;
		
		boolean firstRun = mDb.getTransactionCount() == 0;
		String startDate = firstRun ? mDb.getOldestValidDate(): getOldDate();
        String endDate = mDb.mWriteFormat.format(new Date());
		
		if (mToken != null) {
//		    Log.d("Buxfer", "startDate=" + startDate + " endDate=" +endDate);
		    ArrayList<String> buxIds = new ArrayList<String>();
		    
		    while (count < num) {
		        if (count % 25 == 0) {
		            page++;
		        }
		        
		        HttpGet get = new HttpGet(URI + "transactions.xml?token="+mToken+"&page="+page+"&startDate="+startDate+"&endDate="+endDate);
	            HttpResponse response = client.execute(get);
	            Document doc = builder.parse(response.getEntity().getContent());
	            
	            String status = getTextContentByTagName(doc, "status");
	            
	            if (status.equals("OK")) {
	                if (num == 1) {
	                    num = Integer.parseInt(getTextContentByTagName(doc, "numTransactions"));
//	                    Log.d("Buxfer", "Total transactions = " + num);
	                }
	                NodeList trans = doc.getElementsByTagName("transaction");
	                if (trans.getLength() == 0) return;
	                
//	                Log.d(getClass().getSimpleName(), "\tgot " + trans.getLength() + " transactions");
	                NodeList properties;
	                Node property;
	                String id, accountId, date, desc, type, tags, notes;
	                double amount;
	                
	                for (int i = 0; i < trans.getLength(); i++) {
	                    // reset variables
	                    id = "";
	                    accountId = "";
	                    date = "";
	                    desc = "";
	                    type = "";
	                    tags = "";
	                    notes = "";
	                    status = "";
	                    status = "";
	                    amount = 0;
	                    
	                    properties = trans.item(i).getChildNodes();
	                    for (int j = 0; j < properties.getLength(); j++) {
	                        property = properties.item(j);
	                        
	                        if (property.getNodeName().equals("id")) {
	                            id = getTextContent(property);
	                        } else if (property.getNodeName().equals("accountId")) {
	                            accountId = getTextContent(property);
	                        } else if (property.getNodeName().equals("date")) {
	                            date = getTextContent(property);
	                        } else if (property.getNodeName().equals("description")) {
	                            desc = getTextContent(property);
	                        } else if (property.getNodeName().equals("amount")) {
	                            amount = Double.parseDouble(getTextContent(property));
	                        } else if (property.getNodeName().equals("type")) {
	                            type = getTextContent(property);
	                        } else if (property.getNodeName().equals("tags")) {
	                            tags = getTextContent(property);
	                        } else if (property.getNodeName().equals("extraInfo")) {
	                            notes = getTextContent(property);
	                        } else if (property.getNodeName().equals("status")) {
	                            status = getTextContent(property);
	                        }
	                    }
	                    
	                    // create transaction
	                    if (firstRun) {
	                        //  special case for first run, just create the transaction
	                        mDb.createTransaction(id, accountId, date, desc, type, tags, notes, status, BuxoidDb.TRANS_UPLOADED, amount);
	                    } else {
	                        if (mDb.transactionExists(id)) {
	                            mDb.updateTransaction(id, accountId, type, status, desc, date, tags, notes, BuxoidDb.TRANS_UPLOADED, amount);
	                        } else {
	                            mDb.createTransaction(id, accountId, date, desc, type, tags, notes, status, BuxoidDb.TRANS_UPLOADED, amount);
	                        }
	                    }
	                    
	                    count++;
	                    buxIds.add(id);
	                    
	                    // delete at most one quick transaction that matches this one
	                    mDb.deleteQuickTransaction(accountId, date, amount);
	                }
	            } else {
	                throw new BuxException(status);
	            }
		    }
		    
		    // delete transactions in the date range that were not returned from buxfer
		    mDb.deleteTransactions(buxIds.toArray(new String[buxIds.size()]), startDate);
		} else {
			throw new BuxException("ERROR: Not logged in.");
		}
//		Log.d(getClass().getSimpleName(), "Done.");
	}
	
	/**
	 * Downloads the all of the tags that the user has created.
	 * @throws BuxException
	 * @throws SAXException
	 * @throws IOException
	 */
	public void downloadTags() throws BuxException, SAXException, IOException {
//		Log.d(getClass().getSimpleName(), "Downloading tags...");
		if (mToken != null) {
			// create request
			HttpGet get = new HttpGet(URI + "tags.xml?token=" + mToken);
			// get response
			HttpResponse response = client.execute(get);
			// build xml doc from response
			Document doc = builder.parse(response.getEntity().getContent());
			// get status code
			String status = getTextContentByTagName(doc, "status");
			
			if (status.equals("OK")) {
				// get a list of all tags
				NodeList tags = doc.getElementsByTagName("tag");
//				Log.d(getClass().getSimpleName(), "\tgot " + tags.getLength() + " tags");
				NodeList properties;
				Node property;
				
				String id = null, name = null, parent = null;
				
				// for each tag...
				for (int i = 0; i < tags.getLength(); i++) {
					properties = tags.item(i).getChildNodes();
					
					// get all properties...
					for (int j = 0; j < properties.getLength(); j++) {
						property = properties.item(j);
						
						if (property.getNodeName().equals("id")) {
							id = getTextContent(property);
						} else if (property.getNodeName().equals("name")) {
							name = getTextContent(property);
						} else if (property.getNodeName().equals("parentId")) {
							parent = getTextContent(property);
						}
					}
					// create tag in DB
					mDb.createTag(id, name, parent);
				}
			} else {
				throw new BuxException(status);
			}
		} else {
			throw new BuxException("ERROR: Not logged in.");
		}
//		Log.d(getClass().getSimpleName(), "Done.");
	}
	
	/**
	 * Gets a date 30 days in the past
	 * @return
	 */
	private String getOldDate() {
	    Calendar cal = Calendar.getInstance();
	    cal.add(Calendar.DATE, -30);
        return mDb.mWriteFormat.format(cal.getTime());
	}
	
	/**
	 * Pulls all manual transactions from the DB that have been marked
	 * for upload the creates the command set and sends it on to 
	 * uploadTransaction().
	 * 
	 * @throws BuxException
	 * @throws SAXException
	 * @throws IOException
	 */
	public void uploadManualTransactions() throws BuxException, SAXException, IOException {
		// get cursor over all manual transactions
		Cursor c = mDb.fetchManualTransactions();
		
		if (mToken != null) {
			if (c.getCount() > 0 && c.moveToFirst()) {
				StringBuilder cmd = new StringBuilder();
				String type;
				int status;
				// create command text
				do {
					// start with description
					cmd.append(c.getString(c.getColumnIndexOrThrow(BuxoidDb.TRANS_DESC)));
					
					// add amount type (+ if income, nothing otherwise)
                    type = c.getString(c.getColumnIndexOrThrow(BuxoidDb.TRANS_TYPE));
                    
					if (type.equals("income")) {
                        cmd.append(" +");
                        cmd.append(c.getString(c.getColumnIndexOrThrow(BuxoidDb.TRANS_AMOUNT)));
                    } else {
                        cmd.append(" ");
                        cmd.append(c.getString(c.getColumnIndexOrThrow(BuxoidDb.TRANS_AMOUNT)));
                    }
					
					String [] split = null;
					if (type.equals("loan") || type.equals("sharedbill")) {
					    String notes = c.getString(c.getColumnIndex(BuxoidDb.TRANS_NOTES));
					    split = notes.split("@@");
					    
					    cmd.append(" ");
					    cmd.append(split[0]);
					}
					
					// add tags
					String tags = c.getString(c.getColumnIndexOrThrow(BuxoidDb.TRANS_TAGS));
					if (tags.length() > 0) {
					    cmd.append(" TAGS:");
					    cmd.append(tags);
					}
					
					// add account name
					cmd.append(" ACCT:");
					cmd.append(c.getString(c.getColumnIndexOrThrow(BuxoidDb.ACCOUNT_NAME)));
					
					// add date
					cmd.append(" DATE:");
					cmd.append(c.getString(c.getColumnIndexOrThrow(BuxoidDb.TRANS_DATE)));
					
					// add status if pending
					status = c.getInt(c.getColumnIndexOrThrow(BuxoidDb.TRANS_STATUS));
					if (status == BuxoidDb.TRANS_STATUS_PENDING) {
						cmd.append(" STATUS:pending");
					}
					
					
                    if (type.equals("loan") || type.equals("sharedbill")) {
                        // for loan and sharedbill types, the rest is stored in the transaction note
                        cmd.append(" @@ ");
                        cmd.append(split[1]);
                    }

					// add semicolon to separate entries if this is not the last row
					if (!c.isLast()) {
						cmd.append(";");
					}
				} while (c.moveToNext());
				
				// upload transactions by posting command
//				Log.d("Buxfer", "Upload Command: " + cmd.toString());
				uploadTransaction(cmd.toString());
			}
		}
	}
	
	/**
	 * Uploads a single transaction or transaction set (';' is delimiter).
	 * Currently public to facilitate direct testing from the app.
	 * @param cmd The add_transaction command (documented at Buxfer.com API)
	 * @throws BuxException
	 * @throws SAXException
	 * @throws IOException
	 */
	private void uploadTransaction(String cmd) throws BuxException, SAXException, IOException {
//		Log.d(getClass().getSimpleName(), "Uploading Manual Transactions...");
//		Log.d(getClass().getSimpleName(), "\t" + cmd);
		
		if (mToken != null) {
			HttpPost post = new HttpPost(URI + "add_transaction.xml");
			
			// 
			List<NameValuePair> pairs = new ArrayList<NameValuePair>();
			pairs.add(new BasicNameValuePair("token", mToken));
			pairs.add(new BasicNameValuePair("format", "sms"));
			pairs.add(new BasicNameValuePair("text", cmd));
			post.setEntity(new UrlEncodedFormEntity(pairs, HTTP.UTF_8));
			
			HttpResponse response = client.execute(post);
			Document doc = builder.parse(response.getEntity().getContent());
			
//			Log.d(getClass().getSimpleName(), writeDoc(doc));
			
			String httpStatus = getTextContentByTagName(doc, "status");
			
			if (httpStatus.equals("OK")) {
				NodeList statuses = doc.getElementsByTagName("status");
				NodeList properties;
				Node property;
				
				// since the first status is the HTTP status, ignore it by starting at 1
				for (int i = 1; i < statuses.getLength(); i++) {
					properties = statuses.item(i).getChildNodes();
					for (int j = 0; j < properties.getLength(); j++) {
						property = properties.item(j);
						
						if (property.getNodeName().equals("transactionAdded")) {
							int num = Integer.parseInt(getTextContent(property));
							if (num != 1) {
								throw new BuxException("Unexpected response value: transactionAdded=" + num);
							}
						} else if (property.getNodeName().equals("parseStatus")) {
							String parseStatus = getTextContent(property);
							if (!parseStatus.equals("success")) {
								throw new BuxException("Parse failure on manual transaction upload: parseStatus = " + parseStatus);
							}
						}
					}
				}
			} else {
				throw new BuxException(httpStatus);
			}
		}
		
		mDb.deleteUploadedTrans();
//		Log.d(getClass().getSimpleName(), "done.");
	}
	
	/**
	 * Returns the text content of a node. Using on <this>that</this> will
	 * return "that".
	 * @param n The node
	 * @return A string of the text content of the node, null if none exists.
	 */
	private static String getTextContent(Node n) {
		while (n.hasChildNodes()) {
			n = n.getFirstChild();
		}
		
		if (n.getNodeType() == Node.TEXT_NODE || n.getNodeType() == Node.CDATA_SECTION_NODE) {
			return n.getNodeValue();
		} else {
			return "";
		}
	}
	
	/**
	 * Gets the text content of a tag by searching. Returns the text content
	 * of the first matching node.
	 * 
	 * @param doc The document
	 * @param name The tag name to search for
	 * @return A string of the text content of the node, null if none exists.
	 */
	private static String getTextContentByTagName(Document doc, String name) {
		return getTextContent(doc.getElementsByTagName(name).item(0));
	}
	 
	/**
	 * Used to debug response by returning a string that can be sent to the log
	 */
//	private String writeDoc(Document doc) {
//		sb = new StringBuilder();
//		writeNode((Node) doc.getDocumentElement());
//		return sb.toString();
//	}
//	
//	// used by writeDoc and writeNode to log xml responses
//	private StringBuilder sb;
//	private int tabCount = 0;
//	
//	/**
//	 * Used to debug response by writing a single node
//	 */
//	private void writeNode(Node n) {
//		if (n.getNodeType() == Node.TEXT_NODE || n.getNodeType() == Node.CDATA_SECTION_NODE) {
//			sb.append(n.getNodeValue());
//		} else {
//			sb.append("\n");
//			for (int i = 0; i < tabCount; i++) {
//				sb.append("\t");
//			}
//			tabCount++;
//			// put the opening tag in
//			sb.append("<" + n.getNodeName() + ">");
//		}
//		
//		// write any child nodes
//		if (n.hasChildNodes()) {
//			NodeList children = n.getChildNodes();
//			
//			for (int i = 0; i < children.getLength(); i++) {
//				writeNode(children.item(i));
//			}
//		}
//		
//		// write closing tag
//		if (n.getNodeType() != Node.TEXT_NODE && n.getNodeType() != Node.CDATA_SECTION_NODE) {
//			tabCount--;
//			
//			if (n.hasChildNodes()) {
//				if (n.getFirstChild().getNodeType() != Node.TEXT_NODE && n.getFirstChild().getNodeType() != Node.CDATA_SECTION_NODE) {
//					sb.append("\n");
//					for (int i = 0; i < tabCount; i++) {
//						sb.append("\t");
//					}
//				}
//			}
//			
//			sb.append("</"+n.getNodeName() + ">");
//		}
//	}
}
